
class PathParser {
  /*
   * MoveTo: M, m
   * LineTo: L, l, H, h, V, v
   * Cubic Bézier Curve: C, c, S, s
   * Quadratic Bézier Curve: Q, q, T, t
   * Elliptical Arc Curve: A, a
   * ClosePath: Z, z
   */
  cmds = [
    // name, params
    ['M',2],['m',2],['L',2],['l',2],['H',1],['h',1],['V',1],['v',1],
    ['C',6],['c',6],['S',4],['s',4],['Q',4],['q',4],['T',2],['t',2],
    ['A',7],['a',7],['Z',0],['z',0],
  ];

  /** @param {string} path */
  parse(path) {
    let index = 0;
    let c = '';
    /** @type {number[]} */ let ps = [];
    /** @type {{t:string, p:number[]}[]} */ let results = [];
    let fail = false;

    while (true) {
      let t = this.type(path, index);
      if (t == null) {
        index++;
        continue;
      }

      if (t.type === 'cmd') {
        if (c != '') {
          results.push({
            t:c, p:ps,
          });
        }
        c = t.value[0];
        ps = [];
        index++;
      } else if (t.type === 'number') {
        let nn = this.parseNumber(path, index);
        if (nn == null) {
          console.error(`index=${index}: number ${path.slice(index)} can not be parsed`);
          fail = true;
          break;
        } else {
          ps.push(nn.n);
          index = nn.endIdx;
        }
      } else if (t.type === 'end') {
        c === '' || results.push({
          t:c, p:ps,
        });
        break;
      } else {
        console.error(`index=${index}: '${t.value}' can not be parsed`);
        fail = true;
        break;
      }
    }

    return {
      success: !fail,
      results,
      index: fail ? index : null,
    };
  }

  type(path, index) {
    if (index >= path.length) {
      return {type: 'end'};
    }

    let c = path[index];
    switch(c) {
      case '1': case '2': case '3': case '4': case '5': case '.':
      case '6': case '7': case '8': case '9': case '0': case '-': case '+':
        return {type: 'number'};
      case ' ': case ',': case '\n': case '\t': case '\r':
        return null;
    }

    let cc = this.cmds.filter(cmd => cmd[0] == c);
    if (cc.length) {
      return {type: 'cmd', value: cc[0]};
    } else {
      return {type: 'unknown', value: c};
    }
  }

  /**
   * @param {string} path 
   * @param {number} startIdx 
   */
  parseNumber(path, startIdx) {
    let dsc = false; // has '.'
    let e = false; // has 'e' or 'E'
    let endIdx = startIdx;

    LOOP:
    for (let i = startIdx; i < path.length; i++) {
      let c = path[i];

      switch (c) {
        case '1': case '2': case '3': case '4': case '5':
        case '6': case '7': case '8': case '9': case '0':
          endIdx = i + 1;
          continue;
        case '+': case '-':
          if (i !== startIdx) {
            break LOOP;
          }
          continue;
        case '.':
          if (dsc) {
            break LOOP;
          } else {
            dsc = true;
          }
          endIdx = i + 1;
          continue;
        case 'e': case 'E':
          if (e) {
            break LOOP;
          } else {
            e = true;
          }
          endIdx = i + 1;
          continue;
        default:
          break LOOP;
      }
    }

    let n = parseFloat(path.slice(startIdx, endIdx));
    if (isNaN(n)) {
      return null;
    } else {
      return {
        endIdx,
        n,
      }
    }
  }
}
const parser = new PathParser();
let id = 0;

function create(context, key) {
  switch (key) {
    // main: 让你编辑路径
    case 'main': return createMain(context);
    // view: 画面选项
    case 'view': return createViewPromise(context);
    default: return null;
  }
}

/**
 * @param {(number|boolean)[]} array 
 */
function generatePathText(array) {
  const txs = array.map(v => typeof(v) == 'boolean' ? (v ? '1' : '0') : v + '');
  txs.forEach((tx, index) => {
    if (tx.startsWith('+')) {
      tx = txs[index] = tx.substring(1);
    }

    if (tx.startsWith('0.')) {
      txs[index] = '.' + tx.substring(2);
    } else if (tx.startsWith('-0.')) {
      txs[index] = '-.' + tx.substring(3);
    }
  });
  for (let i = 1; i < txs.length; i++) {
    let tx = txs[i];
    if (tx.startsWith('-')) {
      continue;
    }
    if (tx.startsWith('.') && txs[i - 1].includes('.')) {
      continue;
    }
    txs[i] = ',' + tx;
  }
  return txs.join('');
}

function createMain(context) {
  return {
    template : context.template,
    data : function () {
      return {
        dirty : false,
        paths : [
          { id: '-1', cmd: ['M', 'M'], values: [0, 0] },
          { id: '-2', cmd: ['L', 'L'], values: [1, 1] },
          { id: '-3', cmd: ['L', 'H'], values: [0] },
          { id: '-4', cmd: ['Z', 'Z'], values: [] },
        ],
        pathText : '',
        valid : false,
        autoCommit : false,
        cmds : [
          {
            value: 'None',
            label: '无',
          },
          {
            value: 'Frag',
            label: '路径片段',
          },
          {
            value: 'M',
            label: 'Move',
            children: [
              { value: 'M', label: 'M', },
              { value: 'm', label: 'm', },
            ],
          },
          {
            value: 'L',
            label: 'Line',
            children: [
              { value: 'L', label: 'L', },
              { value: 'l', label: 'l', },
              { value: 'H', label: 'H', },
              { value: 'h', label: 'h', },
              { value: 'V', label: 'V', },
              { value: 'v', label: 'v', },
            ],
          },
          {
            value: 'C',
            label: 'Cubic',
            children: [
              { value: 'C', label: 'C', },
              { value: 'c', label: 'c', },
              { value: 'S', label: 'S', },
              { value: 's', label: 's', },
            ],
          },
          {
            value: 'Q',
            label: 'Quadratic',
            children: [
              { value: 'Q', label: 'Q', },
              { value: 'q', label: 'q', },
              { value: 'T', label: 'T', },
              { value: 't', label: 't', },
            ],
          },
          {
            value: 'E',
            label: 'Elliptical',
            children: [
              { value: 'A', label: 'A', },
              { value: 'a', label: 'a', },
            ],
          },
          {
            value: 'Z',
            label: 'Close Path',
            children: [
              { value: 'Z', label: 'Z', },
              { value: 'z', label: 'z', },
            ],
          },
        ],
        // 模态框, 插入路径片段
        fragIndex : null,
        fragModalShow : false,
        fragModalText : '',
        fragModalPath : '',
        fragModalPathValid : true,

        // 状态数据
        status : null,
      }
    },
    props : {
      sid : String,
    },
    methods : {
      onScreenUpdated : function () {
        let status = this.status = context.getStatus(this.sid);
        if (!status || !status.data) {
          return;
        }
        if (status.data) {
          if (!this.dirty) {
            this.paths = this.generatePath(status.data.paths);
          }
        }
      },
      btnTypeChange : function (item, index) {
        this.dirty = true;
        let newType = item.cmd[1];
        if (newType == null) {
          return;
        }

        const meta = parser.cmds.filter(c => c[0] === newType)[0]
        const newLen = meta[1];
        const oldLen = item.values.length;
        if (newLen == oldLen) {
          return;
        }
        if (newLen > oldLen) {
          for (let i = oldLen; i < newLen; i++) {
            item.values.push(0);
          }
        } else {
          item.values = item.values.slice(0, newLen);
        }

        if (newType == 'A' || newType == 'a') {
          item.values[3] = item.values[4] = false;
        }
      },
      btnAdd : function (index) {
        this.dirty = true;
        const newId = id++;
        this.paths.splice(index, 0, { id: `${newId}`, cmd: ['None'], values: [] });
      },
      btnRemove : function (index) {
        this.dirty = true;
        this.paths.splice(index, 1);
      },
      btnCommit : function () {
        this.submitPath();
      },
      btnRefresh : function () {
        this.paths = this.generatePath(this.status.data.paths);
        this.dirty = false;
      },
      btnFragInsert : function (index) {
        // fragIndex : null,
        // fragModalShow : false,
        // fragModalText : '',
        // fragModalPath : '',
        // fragModalPathValid : true,
        this.fragIndex = index;
        this.fragModalShow = true;
        this.fragModalText = '';
        this.fragModalPath = '';
        this.fragModalPathValid = false;
      },
      // 模态框, 插入路径片段
      btnFragModalOk : function () {
        this.fragModalShow = false;
        if (this.fragModalText == '') {
          return;
        }

        this.paths.splice(this.fragIndex, 1, ...this.generatePath(this.fragModalText));
        this.fragModalText = '';
      },
      btnFragModalCancel : function () {
        this.fragModalShow = false;
      },
      evtFragModalTextChanged : function () {
        if (this.fragModalText === '') {
          this.fragModalPath = '';
          this.fragModalPathValid = false;
          return;
        }

        const results = parser.parse(this.fragModalText);
        this.fragModalPathValid = results.success;
        const list = results.results.map(v => v.t + (v.p.length ? ',' : '') + v.p.join(','));

        if (!results.success) {
          list.push(' ...后面数据有误');
        }
        this.fragModalPath = list.join('\n');
      },

      // compute
      submitPath : function () {
        if (!this.valid) return;
        context.sendCommand(this.sid, 'update', {v : this.pathText});
      },
      generatePath : function(path) {
        let p = parser.parse(path);
        if (!p.success) {
          console.warn('路径数据解析有误', p, path);
        }
        // [
        //   { id: '-1', cmd: ['M', 'M'], values: [0, 0] },
        //   { id: '-2', cmd: ['L', 'L'], values: [1, 1] },
        //   { id: '-3', cmd: ['L', 'H'], values: [0] },
        //   { id: '-4', cmd: ['Z', 'Z'], values: [] },
        // ]
        let paths = [];
        // [{ key: 'M', values: ['M', 'm'] }, ...]
        let cmds = this.cmds
          .filter(c => c.children != null)
          .map(c => {
            return {
              key: c.value,
              values: c.children.map(cc => cc.value)
            };
          });

        for (let i = 0; i < p.results.length; i++) {
          let result = p.results[i];
          let newId = id++;
          let cmd0 = cmds.filter(c => c.values.includes(result.t));

          paths.push({
            id: `${newId}`,
            cmd: [cmd0[0].key, result.t],
            values: (result.t == 'a' || result.t == 'A') ?
              (function _(arr) {
                /** @type {(number | boolean)[]} */
                const a = arr.slice();
                a[3] = arr[3] == 1;
                a[4] = arr[4] == 1;
                return a;
              })(result.p) :
              result.p,
          });
        }

        return paths;
      }
    },
    watch : {
      'paths': {
        handler(newVal, oldVal) {
          // paths : [
          //   { id: '-1', cmd: ['M', 'M'], values: [0, 0] },
          //   { id: '-2', cmd: ['L', 'L'], values: [1, 1] },
          //   { id: '-3', cmd: ['L', 'H'], values: [0] },
          //   { id: '-4', cmd: ['Z', 'Z'], values: [] },
          // ],
          /** @type {string[]} */
          const txs = [];
          this.valid = true;

          for (let i = 0; i < newVal.length; i++) {
            let item = newVal[i];

            if (item.cmd.length != 2) {
              continue;
            }

            const type = item.cmd[1];
            const vs = item.values;
            if (type == 'a' || type == 'A') {
              if (vs.length != 7 || vs.some(v => typeof(v) != 'number' && typeof(v) != 'boolean')) {
                this.valid = false;
                txs.push(' ...后面数据有误');
                break;
              }
              txs.push(type + generatePathText(vs));
              continue;
            }

            const meta = parser.cmds.filter(c => c[0] === type)[0]
            const len = meta[1];
            if (len == 0) {
              txs.push(type);
              continue;
            }

            if (vs.length != len || vs.some(v => typeof(v) != 'number')) {
              this.valid = false;
              txs.push(' ...后面数据有误');
              break;
            }
            txs.push(type + generatePathText(vs));
          }

          this.pathText = txs.join('');
          if (this.valid && this.autoCommit) {
            this.submitPath();
          }
        },
        deep: true,
        immediate: true,
      }
    },
    mounted : function () {
      this.onScreenUpdated(); // 更新 chunkWidth
      context.getBus().$on('screen-status-sync-all', this.onScreenUpdated);
      context.getBus().$on('screen-updated', this.onScreenUpdated);
      context.getBus().$on('screen-destroyed', this.onScreenUpdated);
    },
    destroyed : function () {
      context.getBus().$off('screen-status-sync-all', this.onScreenUpdated);
      context.getBus().$off('screen-updated', this.onScreenUpdated);
      context.getBus().$off('screen-destroyed', this.onScreenUpdated);
    }
  };
}

function createViewPromise(context) {
  return context.fetchHtmlScript('monitor/path_editor_view.html').then(html => {
    return createView(context, html);
  });
}

function createView(context, html) {
  return {
    template : html,
    data : function () {
      return {
        scale : 1,
        originX : 0,
        originY : 0,
        dirty : false,

        // 状态数据
        status : null,
      }
    },
    props : {
      sid : String,
    },
    methods : {
      onScreenUpdated : function () {
        let status = this.status = context.getStatus(this.sid);
        if (!status || !status.data) {
          return;
        }
        if (status.data && !this.dirty) {
          this.refresh();
        }
      },
      btnCommit : function () {
        const { panel } = this.status.data;
        context.sendCommand(this.sid, 'axis', {
          scale : 1 / this.scale,
          originX : (-this.originX + 0.5) * panel[0],
          originY : (-this.originY + 0.5) * panel[1],
        });
        this.dirty = false;
      },
      btnRefresh : function () {
        this.refresh();
      },

      refresh : function () {
        const { scale, origin, panel } = this.status.data;
        this.scale = 1 / scale;
        this.originX = -(origin[0] / panel[0] - 0.5);
        this.originY = -(origin[1] / panel[1] - 0.5);
        this.dirty = false;
      }
    },
    mounted : function () {
      this.onScreenUpdated(); // 更新 chunkWidth
      context.getBus().$on('screen-status-sync-all', this.onScreenUpdated);
      context.getBus().$on('screen-updated', this.onScreenUpdated);
      context.getBus().$on('screen-destroyed', this.onScreenUpdated);
    },
    destroyed : function () {
      context.getBus().$off('screen-status-sync-all', this.onScreenUpdated);
      context.getBus().$off('screen-updated', this.onScreenUpdated);
      context.getBus().$off('screen-destroyed', this.onScreenUpdated);
    }
  };
}

export default {
  factory : create,
  tabs : [{
    key : 'view',
    name : '画面选项',
    icon : 'carry-out'
  }]
};
